fix(relay,store): 审计修复 — 自动恢复可达化 + 类型化错误 + 全路径重连
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

C1: mark_key_429 设 is_active=FALSE,使 select_best_key 自动恢复
路径真正可达。之前 429 只设 cooldown_until,恢复代码为死代码。

H1+H2: 重试查询补全 debug 日志(RPM/TPM 跳过、解密失败)+ 修复
fallthrough 错误信息(RateLimited 而非 NotFound)。

H3+H4+M3+M4+M5: agentStore.ts 提取 classifyAgentError() 类型化错误
分类,覆盖 502/503/401/403/429/500,统一 createClone/
createFromTemplate/updateClone/deleteClone 错误处理,不再泄露原始
错误详情。所有 catch 块添加 log.error。

H5+H6: auth.ts 提取 triggerReconnect() 共享函数,login/loginWithTotp/
restoreSession 三处统一调用。状态检查改为仅 'disconnected' 时触发,
避免 connecting/reconnecting 状态下并发 connect。

M1: toggle_key_active(true) 同步清除 cooldown_until,防止管理员
激活后 key 仍被 cooldown 过滤不可见。
This commit is contained in:
iven
2026-04-19 13:45:49 +08:00
parent d9f8850083
commit e12766794b
3 changed files with 80 additions and 44 deletions

View File

@@ -201,14 +201,23 @@ pub async fn select_best_key(db: &PgPool, provider_id: &str, enc_key: &[u8; 32])
for (id, key_value, _priority, max_rpm, max_tpm, req_count, token_count) in &retry_rows {
if let Some(rpm_limit) = max_rpm {
if *rpm_limit > 0 && req_count.unwrap_or(0) >= *rpm_limit { continue; }
if *rpm_limit > 0 && req_count.unwrap_or(0) >= *rpm_limit {
tracing::debug!("[retry] Reactivated key {} hit RPM limit ({}/{})", id, req_count.unwrap_or(0), rpm_limit);
continue;
}
}
if let Some(tpm_limit) = max_tpm {
if *tpm_limit > 0 && token_count.unwrap_or(0) >= *tpm_limit { continue; }
if *tpm_limit > 0 && token_count.unwrap_or(0) >= *tpm_limit {
tracing::debug!("[retry] Reactivated key {} hit TPM limit ({}/{})", id, token_count.unwrap_or(0), tpm_limit);
continue;
}
}
let decrypted_kv = match decrypt_key_value(key_value, enc_key) {
Ok(v) => v,
Err(_) => continue,
Err(e) => {
tracing::warn!("[retry] Reactivated key {} decryption failed: {}", id, e);
continue;
}
};
let selection = KeySelection {
key: PoolKey { id: id.clone(), key_value: decrypted_kv, priority: *_priority, max_rpm: *max_rpm, max_tpm: *max_tpm },
@@ -220,6 +229,12 @@ pub async fn select_best_key(db: &PgPool, provider_id: &str, enc_key: &[u8; 32])
});
return Ok(selection);
}
// 所有恢复的 Key 仍被 RPM/TPM 限制或解密失败
tracing::warn!("Provider {} 恢复的 Key 全部不可用RPM/TPM 超限或解密失败)", provider_id);
return Err(SaasError::RateLimited(
format!("Provider {} 恢复的 Key 仍在限流中,请稍后重试", provider_id)
));
}
}
@@ -286,14 +301,14 @@ pub async fn mark_key_429(
let now = chrono::Utc::now();
sqlx::query(
"UPDATE provider_keys SET last_429_at = $1, cooldown_until = $2, updated_at = $3
"UPDATE provider_keys SET last_429_at = $1, cooldown_until = $2, is_active = FALSE, updated_at = $3
WHERE id = $4"
)
.bind(&now).bind(&cooldown).bind(&now).bind(key_id)
.execute(db).await?;
tracing::warn!(
"Key {} 收到 429冷却至 {}",
"Key {} 收到 429标记 is_active=FALSE冷却至 {}",
key_id,
cooldown
);
@@ -372,9 +387,16 @@ pub async fn toggle_key_active(
active: bool,
) -> SaasResult<()> {
let now = chrono::Utc::now();
sqlx::query(
"UPDATE provider_keys SET is_active = $1, updated_at = $2 WHERE id = $3"
).bind(active).bind(&now).bind(key_id).execute(db).await?;
// When activating, clear cooldown so the key is immediately selectable
if active {
sqlx::query(
"UPDATE provider_keys SET is_active = $1, cooldown_until = NULL, updated_at = $2 WHERE id = $3"
).bind(active).bind(&now).bind(key_id).execute(db).await?;
} else {
sqlx::query(
"UPDATE provider_keys SET is_active = $1, updated_at = $2 WHERE id = $3"
).bind(active).bind(&now).bind(key_id).execute(db).await?;
}
Ok(())
}