mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 13:10:21 +08:00
修复 ChatGPT Web 生图额度递减显示
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::io::Error as IoError;
|
||||
use std::time::Instant;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use aether_admin::provider::quota::{
|
||||
parse_chatgpt_web_conversation_init_response, quota_refresh_success_invalid_state,
|
||||
@@ -964,6 +964,19 @@ fn spawn_chatgpt_web_image_quota_refresh_after_success(
|
||||
let base_url = base_url.to_string();
|
||||
let token = token.to_string();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = apply_chatgpt_web_image_quota_success_delta(&state, &plan).await {
|
||||
warn!(
|
||||
event_name = "chatgpt_web_image_quota_success_delta_failed",
|
||||
log_type = "ops",
|
||||
request_id = %plan.request_id,
|
||||
candidate_id = ?plan.candidate_id,
|
||||
provider_id = %plan.provider_id,
|
||||
key_id = %plan.key_id,
|
||||
error = %err,
|
||||
"gateway failed to persist ChatGPT-Web image quota success delta"
|
||||
);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
if let Err(err) =
|
||||
refresh_chatgpt_web_image_quota_after_success(&state, &plan, &base_url, &token).await
|
||||
{
|
||||
@@ -981,6 +994,91 @@ fn spawn_chatgpt_web_image_quota_refresh_after_success(
|
||||
});
|
||||
}
|
||||
|
||||
async fn apply_chatgpt_web_image_quota_success_delta(
|
||||
state: &AppState,
|
||||
plan: &ExecutionPlan,
|
||||
) -> Result<bool, String> {
|
||||
let key_id = plan.key_id.trim();
|
||||
let provider_id = plan.provider_id.trim();
|
||||
let Some(mut latest_key) = state
|
||||
.read_provider_catalog_keys_by_ids(&[key_id.to_string()])
|
||||
.await
|
||||
.map_err(|err| err.into_message())?
|
||||
.into_iter()
|
||||
.find(|key| key.id == key_id && key.provider_id == provider_id)
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let Some(mut metadata) = latest_key
|
||||
.upstream_metadata
|
||||
.as_ref()
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|metadata| metadata.get("chatgpt_web"))
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let now_unix_secs = current_unix_secs();
|
||||
if chatgpt_web_image_quota_u64(metadata.get("image_quota_reset_at"))
|
||||
.is_some_and(|reset_at| reset_at <= now_unix_secs)
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let Some(remaining) = chatgpt_web_image_quota_f64(metadata.get("image_quota_remaining"))
|
||||
.filter(|value| *value > 0.0)
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let limit = chatgpt_web_image_quota_f64(metadata.get("image_quota_total"))
|
||||
.filter(|value| *value > 0.0)
|
||||
.unwrap_or(remaining);
|
||||
let new_remaining = (remaining - 1.0).max(0.0);
|
||||
|
||||
metadata.insert("image_quota_remaining".to_string(), json!(new_remaining));
|
||||
metadata.insert("image_quota_total".to_string(), json!(limit));
|
||||
metadata.insert(
|
||||
"image_quota_used".to_string(),
|
||||
json!((limit - new_remaining).max(0.0)),
|
||||
);
|
||||
metadata.insert("updated_at".to_string(), json!(now_unix_secs));
|
||||
metadata.insert(
|
||||
"image_quota_last_local_success_at".to_string(),
|
||||
json!(now_unix_secs),
|
||||
);
|
||||
let local_success_count =
|
||||
chatgpt_web_image_quota_u64(metadata.get("image_quota_local_success_count")).unwrap_or(0);
|
||||
metadata.insert(
|
||||
"image_quota_local_success_count".to_string(),
|
||||
json!(local_success_count.saturating_add(1)),
|
||||
);
|
||||
|
||||
let updated_upstream_metadata = merge_provider_metadata_object(
|
||||
latest_key.upstream_metadata.as_ref(),
|
||||
"chatgpt_web",
|
||||
Value::Object(metadata),
|
||||
);
|
||||
latest_key.upstream_metadata = updated_upstream_metadata;
|
||||
latest_key.status_snapshot = sync_provider_key_quota_status_snapshot(
|
||||
latest_key.status_snapshot.as_ref(),
|
||||
"chatgpt_web",
|
||||
latest_key.upstream_metadata.as_ref(),
|
||||
"image_success_local",
|
||||
);
|
||||
latest_key.status_snapshot =
|
||||
sync_provider_key_oauth_status_snapshot(latest_key.status_snapshot.as_ref(), &latest_key);
|
||||
latest_key.updated_at_unix_secs = Some(now_unix_secs);
|
||||
|
||||
Ok(state
|
||||
.update_provider_catalog_key_runtime_state(&latest_key)
|
||||
.await
|
||||
.map_err(|err| err.into_message())?
|
||||
.is_some())
|
||||
}
|
||||
|
||||
async fn refresh_chatgpt_web_image_quota_after_success(
|
||||
state: &AppState,
|
||||
plan: &ExecutionPlan,
|
||||
@@ -1039,8 +1137,9 @@ async fn refresh_chatgpt_web_image_quota_after_success(
|
||||
}
|
||||
|
||||
let body_json = execution_result_json(&result).map_err(|err| err.to_string())?;
|
||||
let now_unix_secs = current_unix_secs();
|
||||
let Some(mut metadata) =
|
||||
parse_chatgpt_web_conversation_init_response(&body_json, current_unix_secs())
|
||||
parse_chatgpt_web_conversation_init_response(&body_json, now_unix_secs)
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
@@ -1055,6 +1154,11 @@ async fn refresh_chatgpt_web_image_quota_after_success(
|
||||
return Ok(false);
|
||||
};
|
||||
normalize_chatgpt_web_image_quota_limit(&mut metadata, latest_key.upstream_metadata.as_ref());
|
||||
preserve_chatgpt_web_local_success_delta(
|
||||
&mut metadata,
|
||||
latest_key.upstream_metadata.as_ref(),
|
||||
now_unix_secs,
|
||||
);
|
||||
|
||||
let mut updated_key = latest_key;
|
||||
let updated_upstream_metadata = merge_provider_metadata_object(
|
||||
@@ -1075,7 +1179,7 @@ async fn refresh_chatgpt_web_image_quota_after_success(
|
||||
);
|
||||
updated_key.status_snapshot =
|
||||
sync_provider_key_oauth_status_snapshot(updated_key.status_snapshot.as_ref(), &updated_key);
|
||||
updated_key.updated_at_unix_secs = Some(current_unix_secs());
|
||||
updated_key.updated_at_unix_secs = Some(now_unix_secs);
|
||||
|
||||
Ok(state
|
||||
.update_provider_catalog_key_runtime_state(&updated_key)
|
||||
@@ -1084,6 +1188,83 @@ async fn refresh_chatgpt_web_image_quota_after_success(
|
||||
.is_some())
|
||||
}
|
||||
|
||||
fn preserve_chatgpt_web_local_success_delta(
|
||||
metadata: &mut Value,
|
||||
existing_upstream_metadata: Option<&Value>,
|
||||
now_unix_secs: u64,
|
||||
) {
|
||||
let Some(incoming) = metadata.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
let Some(existing) = existing_upstream_metadata
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|metadata| metadata.get("chatgpt_web"))
|
||||
.and_then(Value::as_object)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(local_success_at) =
|
||||
chatgpt_web_image_quota_u64(existing.get("image_quota_last_local_success_at"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if now_unix_secs.saturating_sub(local_success_at) > 180 {
|
||||
return;
|
||||
}
|
||||
|
||||
let incoming_reset_at = chatgpt_web_image_quota_u64(incoming.get("image_quota_reset_at"));
|
||||
let existing_reset_at = chatgpt_web_image_quota_u64(existing.get("image_quota_reset_at"));
|
||||
if incoming_reset_at.is_some_and(|reset_at| reset_at <= now_unix_secs) {
|
||||
return;
|
||||
}
|
||||
match (incoming_reset_at, existing_reset_at) {
|
||||
(Some(incoming_reset_at), Some(existing_reset_at))
|
||||
if incoming_reset_at != existing_reset_at =>
|
||||
{
|
||||
return;
|
||||
}
|
||||
(Some(_), Some(_)) | (None, None) => {}
|
||||
_ => return,
|
||||
}
|
||||
|
||||
let Some(existing_remaining) =
|
||||
chatgpt_web_image_quota_f64(existing.get("image_quota_remaining"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(incoming_remaining) =
|
||||
chatgpt_web_image_quota_f64(incoming.get("image_quota_remaining"))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if incoming_remaining <= existing_remaining {
|
||||
return;
|
||||
}
|
||||
|
||||
let limit = chatgpt_web_image_quota_f64(incoming.get("image_quota_total"))
|
||||
.filter(|value| *value > 0.0)
|
||||
.or_else(|| {
|
||||
chatgpt_web_image_quota_f64(existing.get("image_quota_total"))
|
||||
.filter(|value| *value > 0.0)
|
||||
})
|
||||
.unwrap_or_else(|| incoming_remaining.max(existing_remaining));
|
||||
incoming.insert(
|
||||
"image_quota_remaining".to_string(),
|
||||
json!(existing_remaining),
|
||||
);
|
||||
incoming.insert("image_quota_total".to_string(), json!(limit));
|
||||
incoming.insert(
|
||||
"image_quota_used".to_string(),
|
||||
json!((limit - existing_remaining).max(0.0)),
|
||||
);
|
||||
if let Some(value) = existing.get("image_quota_last_local_success_at").cloned() {
|
||||
incoming.insert("image_quota_last_local_success_at".to_string(), value);
|
||||
}
|
||||
if let Some(value) = existing.get("image_quota_local_success_count").cloned() {
|
||||
incoming.insert("image_quota_local_success_count".to_string(), value);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_chatgpt_web_image_quota_refresh_plan(
|
||||
plan: &ExecutionPlan,
|
||||
spec: ProviderPoolQuotaRequestSpec,
|
||||
@@ -1169,6 +1350,26 @@ fn merge_provider_metadata_object(
|
||||
Some(Value::Object(merged))
|
||||
}
|
||||
|
||||
fn chatgpt_web_image_quota_f64(value: Option<&Value>) -> Option<f64> {
|
||||
match value {
|
||||
Some(Value::Number(number)) => number.as_f64(),
|
||||
Some(Value::String(value)) => value.trim().parse::<f64>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
.filter(|value| value.is_finite())
|
||||
}
|
||||
|
||||
fn chatgpt_web_image_quota_u64(value: Option<&Value>) -> Option<u64> {
|
||||
let mut parsed = chatgpt_web_image_quota_f64(value)?;
|
||||
if parsed <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
if parsed > 1_000_000_000_000.0 {
|
||||
parsed /= 1000.0;
|
||||
}
|
||||
Some(parsed.floor() as u64)
|
||||
}
|
||||
|
||||
fn chatgpt_web_image_transport_profile(plan: &ExecutionPlan) -> Option<ResolvedTransportProfile> {
|
||||
match plan.transport_profile.as_ref() {
|
||||
Some(profile)
|
||||
@@ -2386,6 +2587,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chatgpt_web_image_quota_refresh_preserves_recent_local_success_delta() {
|
||||
let mut metadata = json!({
|
||||
"updated_at": 1_000u64,
|
||||
"image_quota_remaining": 25.0,
|
||||
"image_quota_total": 25.0,
|
||||
"image_quota_used": 0.0,
|
||||
"image_quota_reset_at": 2_000u64
|
||||
});
|
||||
let existing = json!({
|
||||
"chatgpt_web": {
|
||||
"updated_at": 995u64,
|
||||
"image_quota_remaining": 24.0,
|
||||
"image_quota_total": 25.0,
|
||||
"image_quota_used": 1.0,
|
||||
"image_quota_reset_at": 2_000u64,
|
||||
"image_quota_last_local_success_at": 998u64,
|
||||
"image_quota_local_success_count": 1u64
|
||||
}
|
||||
});
|
||||
|
||||
preserve_chatgpt_web_local_success_delta(&mut metadata, Some(&existing), 1_000);
|
||||
|
||||
assert_eq!(metadata["image_quota_remaining"], json!(24.0));
|
||||
assert_eq!(metadata["image_quota_total"], json!(25.0));
|
||||
assert_eq!(metadata["image_quota_used"], json!(1.0));
|
||||
assert_eq!(metadata["image_quota_last_local_success_at"], json!(998u64));
|
||||
}
|
||||
|
||||
async fn start_mock_chatgpt_web() -> (String, tokio::task::JoinHandle<()>) {
|
||||
let app = Router::new().fallback(any(|request: Request| async move {
|
||||
let path = request.uri().path().to_string();
|
||||
|
||||
@@ -438,16 +438,6 @@ fn chatgpt_web_image_quota_limit(
|
||||
metadata: &Map<String, Value>,
|
||||
remaining: Option<f64>,
|
||||
) -> Option<f64> {
|
||||
let plan_type = metadata
|
||||
.get("plan_type")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| value.to_ascii_lowercase());
|
||||
if plan_type.as_deref() == Some("free") {
|
||||
return Some(25.0);
|
||||
}
|
||||
|
||||
let explicit_limit = metadata
|
||||
.get("image_quota_total")
|
||||
.and_then(admin_provider_quota_pure::coerce_json_f64)
|
||||
@@ -2535,12 +2525,12 @@ mod tests {
|
||||
assert_eq!(quota.get("code"), Some(&json!("ok")));
|
||||
assert_eq!(quota.get("plan_type"), Some(&json!("free")));
|
||||
assert_eq!(quota.get("reset_at"), Some(&json!(1_778_157_172u64)));
|
||||
assert_eq!(quota.get("usage_ratio"), Some(&json!(0.04)));
|
||||
assert_eq!(quota.get("usage_ratio"), Some(&json!(0.0)));
|
||||
assert_eq!(window.get("code"), Some(&json!("image_gen")));
|
||||
assert_eq!(window.get("remaining_value"), Some(&json!(24.0)));
|
||||
assert_eq!(window.get("limit_value"), Some(&json!(25.0)));
|
||||
assert_eq!(window.get("used_value"), Some(&json!(1.0)));
|
||||
assert_eq!(window.get("remaining_ratio"), Some(&json!(0.96)));
|
||||
assert_eq!(window.get("limit_value"), Some(&json!(24.0)));
|
||||
assert_eq!(window.get("used_value"), Some(&json!(0.0)));
|
||||
assert_eq!(window.get("remaining_ratio"), Some(&json!(1.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -212,7 +212,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chatgpt_web_quota_metadata_enriches_auth_and_normalizes_free_limit() {
|
||||
fn chatgpt_web_quota_metadata_enriches_auth_and_uses_first_remaining_as_limit() {
|
||||
let mut metadata = json!({
|
||||
"image_quota_remaining": 12,
|
||||
});
|
||||
@@ -229,8 +229,8 @@ mod tests {
|
||||
assert_eq!(metadata["plan_type"], json!("free"));
|
||||
assert_eq!(metadata["email"], json!("user@example.com"));
|
||||
assert_eq!(metadata["account_id"], json!("acct-1"));
|
||||
assert_eq!(metadata["image_quota_total"], json!(25.0));
|
||||
assert_eq!(metadata["image_quota_used"], json!(13.0));
|
||||
assert_eq!(metadata["image_quota_total"], json!(12.0));
|
||||
assert_eq!(metadata["image_quota_used"], json!(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -24,8 +24,6 @@ const CHATGPT_WEB_CLIENT_VERSION: &str = "prod-be885abbfcfe7b1f511e88b3003d9ee44
|
||||
const CHATGPT_WEB_BUILD_NUMBER: &str = "5955942";
|
||||
const CHATGPT_WEB_SEC_CH_UA: &str =
|
||||
r#""Microsoft Edge";v="143", "Chromium";v="143", "Not A(Brand";v="24""#;
|
||||
const CHATGPT_WEB_FREE_IMAGE_QUOTA_LIMIT: f64 = 25.0;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ChatGptWebProviderPoolAdapter;
|
||||
|
||||
@@ -185,14 +183,8 @@ pub fn normalize_chatgpt_web_image_quota_limit(
|
||||
let remaining = provider_pool_json_f64(object.get("image_quota_remaining"));
|
||||
let explicit_limit =
|
||||
provider_pool_json_f64(object.get("image_quota_total")).filter(|value| *value > 0.0);
|
||||
let plan_type = chatgpt_web_json_string(object.get("plan_type"));
|
||||
let is_free_plan = plan_type.is_some_and(|value| value.trim().eq_ignore_ascii_case("free"));
|
||||
let limit = if is_free_plan {
|
||||
Some(CHATGPT_WEB_FREE_IMAGE_QUOTA_LIMIT)
|
||||
} else {
|
||||
explicit_limit
|
||||
.or_else(|| infer_chatgpt_web_image_quota_limit(plan_type, remaining, existing_limit))
|
||||
};
|
||||
let limit =
|
||||
explicit_limit.or_else(|| infer_chatgpt_web_image_quota_limit(remaining, existing_limit));
|
||||
|
||||
if let Some(limit) = limit {
|
||||
object.insert("image_quota_total".to_string(), json!(limit));
|
||||
@@ -222,13 +214,6 @@ fn chatgpt_web_auth_config_string(auth_config: Option<&Value>, fields: &[&str])
|
||||
})
|
||||
}
|
||||
|
||||
fn chatgpt_web_json_string(value: Option<&Value>) -> Option<&str> {
|
||||
value
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn existing_chatgpt_web_image_quota_limit(upstream_metadata: Option<&Value>) -> Option<f64> {
|
||||
upstream_metadata
|
||||
.and_then(Value::as_object)
|
||||
@@ -239,15 +224,9 @@ fn existing_chatgpt_web_image_quota_limit(upstream_metadata: Option<&Value>) ->
|
||||
}
|
||||
|
||||
fn infer_chatgpt_web_image_quota_limit(
|
||||
plan_type: Option<&str>,
|
||||
remaining: Option<f64>,
|
||||
existing_limit: Option<f64>,
|
||||
) -> Option<f64> {
|
||||
let normalized_plan = plan_type.unwrap_or_default().trim().to_ascii_lowercase();
|
||||
if normalized_plan == "free" {
|
||||
return Some(CHATGPT_WEB_FREE_IMAGE_QUOTA_LIMIT);
|
||||
}
|
||||
|
||||
if let Some(existing_limit) = existing_limit.filter(|value| *value > 0.0) {
|
||||
return Some(existing_limit);
|
||||
}
|
||||
|
||||
@@ -1056,7 +1056,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span class="text-muted-foreground">使用额度</span>
|
||||
<span class="text-muted-foreground">剩余额度</span>
|
||||
<span :class="getQuotaRemainingClass(getChatGPTWebQuotaUsedPercent(key))">
|
||||
{{ getChatGPTWebQuotaRemainingPercent(key).toFixed(1) }}%
|
||||
</span>
|
||||
@@ -1070,7 +1070,7 @@
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-[9px] text-muted-foreground/70 mt-0.5">
|
||||
<span>
|
||||
{{ formatChatGPTWebUsage(getChatGPTWebQuotaDisplay(key)?.image_quota_used) }} /
|
||||
{{ formatChatGPTWebUsage(getChatGPTWebQuotaDisplay(key)?.image_quota_remaining) }} /
|
||||
{{ formatChatGPTWebUsage(getChatGPTWebQuotaDisplay(key)?.image_quota_total) }}
|
||||
</span>
|
||||
<span v-if="getChatGPTWebQuotaDisplay(key)?.image_quota_reset_at">
|
||||
|
||||
@@ -105,6 +105,28 @@ describe('providerKeyQuota', () => {
|
||||
}, 'grok')).toBe('Auto剩余 40.0% (60/150) | Heavy剩余 0.0% (0/20)')
|
||||
})
|
||||
|
||||
it('formats ChatGPT Web image quota as remaining count', () => {
|
||||
expect(getQuotaDisplayText({
|
||||
status_snapshot: {
|
||||
quota: {
|
||||
provider_type: 'chatgpt_web',
|
||||
code: 'ok',
|
||||
exhausted: false,
|
||||
windows: [
|
||||
{
|
||||
code: 'image_gen',
|
||||
scope: 'account',
|
||||
remaining_ratio: 0.96,
|
||||
used_value: 1,
|
||||
remaining_value: 24,
|
||||
limit_value: 25,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}, 'chatgpt_web')).toBe('生图剩余 24/25')
|
||||
})
|
||||
|
||||
it('surfaces Windsurf hard account states', () => {
|
||||
expect(getQuotaDisplayText({
|
||||
status_snapshot: {
|
||||
|
||||
@@ -343,19 +343,13 @@ function getChatGPTWebQuotaText(quota: QuotaStatusSnapshot): string | null {
|
||||
if (!window) return normalizeText(quota.label)
|
||||
|
||||
const remainingPercent = getQuotaWindowRemainingPercent(window)
|
||||
if (typeof window.remaining_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0 && window.remaining_value <= 0) {
|
||||
return `生图剩余 ${formatQuotaValue(window.remaining_value)}/${formatQuotaValue(window.limit_value)}`
|
||||
}
|
||||
if (remainingPercent != null) {
|
||||
if (typeof window.used_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0) {
|
||||
return `生图剩余 ${formatPercent(remainingPercent)} (${formatQuotaValue(window.used_value)}/${formatQuotaValue(window.limit_value)})`
|
||||
}
|
||||
return `生图剩余 ${formatPercent(remainingPercent)}`
|
||||
}
|
||||
|
||||
if (typeof window.remaining_value === 'number' && typeof window.limit_value === 'number' && window.limit_value > 0) {
|
||||
return `生图剩余 ${formatQuotaValue(window.remaining_value)}/${formatQuotaValue(window.limit_value)}`
|
||||
}
|
||||
if (remainingPercent != null) {
|
||||
return `生图剩余 ${formatPercent(remainingPercent)}`
|
||||
}
|
||||
|
||||
if (typeof window.remaining_value === 'number') {
|
||||
return `生图剩余 ${formatQuotaValue(window.remaining_value)}`
|
||||
}
|
||||
|
||||
@@ -4109,10 +4109,10 @@ function buildQuotaProgressItemsFromSnapshot(key: PoolKeyDetail): QuotaProgressI
|
||||
const remainingValue = typeof window?.remaining_value === 'number' ? window.remaining_value : null
|
||||
const limitValue = typeof window?.limit_value === 'number' ? window.limit_value : null
|
||||
const usedValue = typeof window?.used_value === 'number' ? window.used_value : null
|
||||
const detail = usedValue != null && limitValue != null
|
||||
? `${formatQuotaValue(usedValue)}/${formatQuotaValue(limitValue)}`
|
||||
: remainingValue != null && limitValue != null
|
||||
? `${formatQuotaValue(Math.max(limitValue - remainingValue, 0))}/${formatQuotaValue(limitValue)}`
|
||||
const detail = remainingValue != null && limitValue != null
|
||||
? `${formatQuotaValue(remainingValue)}/${formatQuotaValue(limitValue)}`
|
||||
: usedValue != null && limitValue != null
|
||||
? `${formatQuotaValue(Math.max(limitValue - usedValue, 0))}/${formatQuotaValue(limitValue)}`
|
||||
: remainingValue != null
|
||||
? `剩余 ${formatQuotaValue(remainingValue)}`
|
||||
: undefined
|
||||
|
||||
Reference in New Issue
Block a user