mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 21:20:20 +08:00
缓存问题修复
This commit is contained in:
@@ -85,6 +85,7 @@ fn merge_codex_reset_credit_detail_metadata(
|
||||
|
||||
fn mark_codex_reset_credit_detail_failed(
|
||||
codex_metadata: &mut Map<String, Value>,
|
||||
updated_at_unix_secs: u64,
|
||||
detail_error: impl Into<String>,
|
||||
) {
|
||||
let mut reset_credits = codex_metadata
|
||||
@@ -92,6 +93,7 @@ fn mark_codex_reset_credit_detail_failed(
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
reset_credits.insert("updated_at".to_string(), json!(updated_at_unix_secs));
|
||||
reset_credits.insert("detail_source".to_string(), json!("wham_readonly"));
|
||||
reset_credits.insert("detail_status".to_string(), json!("failed"));
|
||||
reset_credits.insert(
|
||||
@@ -116,7 +118,7 @@ async fn enrich_codex_reset_credit_details(
|
||||
{
|
||||
Ok(request_spec) => request_spec,
|
||||
Err(message) => {
|
||||
mark_codex_reset_credit_detail_failed(codex_metadata, message);
|
||||
mark_codex_reset_credit_detail_failed(codex_metadata, now_unix_secs, message);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
@@ -129,6 +131,7 @@ async fn enrich_codex_reset_credit_details(
|
||||
ProviderQuotaExecutionOutcome::Failure(detail) => {
|
||||
mark_codex_reset_credit_detail_failed(
|
||||
codex_metadata,
|
||||
now_unix_secs,
|
||||
format!("reset credit detail 请求执行失败: {detail}"),
|
||||
);
|
||||
return Ok(());
|
||||
@@ -140,6 +143,7 @@ async fn enrich_codex_reset_credit_details(
|
||||
.unwrap_or_else(|| format!("HTTP {}", result.status_code));
|
||||
mark_codex_reset_credit_detail_failed(
|
||||
codex_metadata,
|
||||
now_unix_secs,
|
||||
format!(
|
||||
"reset credit detail 返回状态码 {}: {detail}",
|
||||
result.status_code
|
||||
@@ -153,7 +157,11 @@ async fn enrich_codex_reset_credit_details(
|
||||
.as_ref()
|
||||
.and_then(|body| body.json_body.as_ref())
|
||||
else {
|
||||
mark_codex_reset_credit_detail_failed(codex_metadata, "无法解析 reset credit detail 响应");
|
||||
mark_codex_reset_credit_detail_failed(
|
||||
codex_metadata,
|
||||
now_unix_secs,
|
||||
"无法解析 reset credit detail 响应",
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(detail_metadata) =
|
||||
@@ -161,7 +169,11 @@ async fn enrich_codex_reset_credit_details(
|
||||
{
|
||||
merge_codex_reset_credit_detail_metadata(codex_metadata, &detail_metadata);
|
||||
} else {
|
||||
mark_codex_reset_credit_detail_failed(codex_metadata, "reset credit detail 响应为空");
|
||||
mark_codex_reset_credit_detail_failed(
|
||||
codex_metadata,
|
||||
now_unix_secs,
|
||||
"reset credit detail 响应为空",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -792,4 +804,19 @@ mod tests {
|
||||
Some(&json!(2u64))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_reset_credit_detail_failure_records_attempt_time() {
|
||||
let mut metadata = Map::new();
|
||||
|
||||
mark_codex_reset_credit_detail_failed(&mut metadata, 1_777_000_000, "request failed");
|
||||
|
||||
assert_eq!(
|
||||
metadata
|
||||
.get("reset_credits")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|credits| credits.get("updated_at")),
|
||||
Some(&json!(1_777_000_000u64))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2054,6 +2054,29 @@ fn quota_snapshot_has_materialized_data(
|
||||
})
|
||||
}
|
||||
|
||||
fn codex_upstream_metadata_is_at_least_as_fresh(
|
||||
quota_snapshot: Option<&Map<String, Value>>,
|
||||
upstream_metadata: Option<&Value>,
|
||||
) -> bool {
|
||||
let Some(metadata) = provider_quota_metadata_bucket(upstream_metadata, "codex") else {
|
||||
return false;
|
||||
};
|
||||
let Some(metadata_updated_at) = metadata
|
||||
.get("updated_at")
|
||||
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let snapshot_updated_at = quota_snapshot.and_then(|quota| {
|
||||
quota
|
||||
.get("updated_at")
|
||||
.or_else(|| quota.get("observed_at"))
|
||||
.and_then(admin_provider_quota_pure::coerce_json_u64)
|
||||
});
|
||||
|
||||
snapshot_updated_at.is_none_or(|updated_at| metadata_updated_at >= updated_at)
|
||||
}
|
||||
|
||||
fn windsurf_quota_snapshot_has_stale_cooldown(quota_snapshot: &Map<String, Value>) -> bool {
|
||||
let code = quota_snapshot
|
||||
.get("code")
|
||||
@@ -2121,8 +2144,15 @@ pub(crate) fn provider_key_status_snapshot_payload(
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|snapshot| snapshot.get("quota"))
|
||||
.and_then(Value::as_object);
|
||||
let refresh_codex_snapshot = provider_type.trim().eq_ignore_ascii_case("codex")
|
||||
&& codex_upstream_metadata_is_at_least_as_fresh(
|
||||
quota_snapshot,
|
||||
key.upstream_metadata.as_ref(),
|
||||
);
|
||||
|
||||
let payload = if quota_snapshot_has_materialized_data(quota_snapshot, provider_type) {
|
||||
let payload = if quota_snapshot_has_materialized_data(quota_snapshot, provider_type)
|
||||
&& !refresh_codex_snapshot
|
||||
{
|
||||
status_snapshot
|
||||
.cloned()
|
||||
.unwrap_or_else(default_provider_key_status_snapshot)
|
||||
@@ -3512,6 +3542,58 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_key_status_snapshot_payload_restores_complete_codex_cache() {
|
||||
let mut key = sample_catalog_key();
|
||||
key.upstream_metadata = Some(json!({
|
||||
"codex": {
|
||||
"updated_at": 200u64,
|
||||
"plan_type": "plus",
|
||||
"primary_used_percent": 89.0,
|
||||
"primary_reset_at": 1_900_000_000u64,
|
||||
"spark_primary_used_percent": 40.0,
|
||||
"spark_primary_reset_at": 1_900_100_000u64,
|
||||
"reset_credits": {
|
||||
"available_count": 3,
|
||||
"updated_at": 200u64,
|
||||
"detail_status": "available",
|
||||
"credits": []
|
||||
}
|
||||
}
|
||||
}));
|
||||
key.status_snapshot = Some(json!({
|
||||
"quota": {
|
||||
"version": 2,
|
||||
"provider_type": "codex",
|
||||
"updated_at": 200u64,
|
||||
"windows": [{
|
||||
"code": "weekly",
|
||||
"used_ratio": 1.0,
|
||||
"reset_at": 1_900_000_000u64
|
||||
}]
|
||||
}
|
||||
}));
|
||||
|
||||
let payload = provider_key_status_snapshot_payload(&key, "codex");
|
||||
let windows = payload["quota"]["windows"]
|
||||
.as_array()
|
||||
.expect("quota windows should exist");
|
||||
|
||||
assert_eq!(payload.pointer("/quota/plan_type"), Some(&json!("plus")));
|
||||
assert_eq!(
|
||||
payload.pointer("/quota/reset_credits/available_count"),
|
||||
Some(&json!(3u64))
|
||||
);
|
||||
assert!(windows.iter().any(|window| window["code"] == "spark_5h"));
|
||||
assert_eq!(
|
||||
windows
|
||||
.iter()
|
||||
.find(|window| window["code"] == "weekly")
|
||||
.and_then(|window| window.get("used_ratio")),
|
||||
Some(&json!(0.89))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_provider_key_quota_status_snapshot_preserves_codex_usage_state() {
|
||||
let current_status_snapshot = json!({
|
||||
|
||||
@@ -1020,6 +1020,7 @@ import {
|
||||
getCodexResetCreditAvailableCount as getCodexResetCreditAvailableCountFromSnapshot,
|
||||
getVisibleCodexResetCreditItems as getVisibleCodexResetCreditItemsFromSnapshot,
|
||||
mergeCodexQuotaDisplays,
|
||||
shouldRefreshMissingCodexResetCredits,
|
||||
} from './codex-reset-credit-display'
|
||||
|
||||
// 扩展端点类型,包含密钥列表
|
||||
@@ -2443,8 +2444,12 @@ function shouldAutoRefreshCodexQuota(): boolean {
|
||||
|
||||
if (isTokenExpiringSoon(key, now)) return true
|
||||
|
||||
// 旧缓存可能已有普通配额但没有 reset-credit 数据,仍需补拉一次
|
||||
if (!hasCodexQuotaDisplayData(key) || getCodexResetCreditAvailableCount(key) === null) {
|
||||
// 旧缓存缺少 reset-credit 数据时补拉;近期已检查过则等待缓存过期
|
||||
if (!hasCodexQuotaDisplayData(key) || shouldRefreshMissingCodexResetCredits(
|
||||
getCodexResetCreditsDisplay(key),
|
||||
now,
|
||||
AUTO_QUOTA_REFRESH_STALE_SECONDS,
|
||||
)) {
|
||||
return true
|
||||
}
|
||||
// 配额数据超过 5 分钟未更新,也触发刷新
|
||||
|
||||
+16
@@ -7,6 +7,7 @@ import {
|
||||
getCodexResetCreditAvailableCount,
|
||||
getVisibleCodexResetCreditItems,
|
||||
mergeCodexQuotaDisplays,
|
||||
shouldRefreshMissingCodexResetCredits,
|
||||
} from '@/features/providers/components/codex-reset-credit-display'
|
||||
import type { QuotaResetCreditsSnapshot } from '@/api/endpoints/types'
|
||||
|
||||
@@ -59,6 +60,21 @@ describe('codex reset credit display helpers', () => {
|
||||
expect(getVisibleCodexResetCreditItems(snapshot, 1_700_000_000)).toEqual([])
|
||||
})
|
||||
|
||||
it('retries missing reset credits only when the cached detail check is stale', () => {
|
||||
expect(shouldRefreshMissingCodexResetCredits(null, 1_700_000_000)).toBe(true)
|
||||
expect(shouldRefreshMissingCodexResetCredits({
|
||||
detail_status: 'failed',
|
||||
updated_at: 1_699_999_900,
|
||||
}, 1_700_000_000)).toBe(false)
|
||||
expect(shouldRefreshMissingCodexResetCredits({
|
||||
detail_status: 'failed',
|
||||
updated_at: 1_699_999_600,
|
||||
}, 1_700_000_000)).toBe(true)
|
||||
expect(shouldRefreshMissingCodexResetCredits({
|
||||
available_count: 0,
|
||||
}, 1_700_000_000)).toBe(false)
|
||||
})
|
||||
|
||||
it('sorts available detail items by remaining time and labels visible items with short ordinal keys', () => {
|
||||
const snapshot: QuotaResetCreditsSnapshot = {
|
||||
available_count: 7,
|
||||
|
||||
@@ -57,6 +57,18 @@ export function getCodexResetCreditAvailableCount(
|
||||
return typeof count === 'number' && Number.isFinite(count) && count >= 0 ? count : null
|
||||
}
|
||||
|
||||
export function shouldRefreshMissingCodexResetCredits(
|
||||
snapshot: QuotaResetCreditsSnapshot | null | undefined,
|
||||
nowUnixSecs = Math.floor(Date.now() / 1000),
|
||||
staleSeconds = 5 * 60,
|
||||
): boolean {
|
||||
if (getCodexResetCreditAvailableCount(snapshot) !== null) return false
|
||||
|
||||
const updatedAt = Number(snapshot?.updated_at)
|
||||
if (!Number.isFinite(updatedAt)) return true
|
||||
return (nowUnixSecs - updatedAt) > staleSeconds
|
||||
}
|
||||
|
||||
export function formatCodexResetCreditCount(count: number | null | undefined): string {
|
||||
return `共 ${count ?? 0} 次机会`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user