Codex 重置功能和显示修复

This commit is contained in:
ZheFox
2026-07-16 17:23:41 +08:00
parent 1333efdad5
commit 5cf38d1b35
5 changed files with 199 additions and 48 deletions
+123 -23
View File
@@ -787,6 +787,10 @@ fn parse_codex_reset_credit_timestamp(value: Option<&serde_json::Value>) -> Opti
}
fn codex_reset_credit_detail_items(value: &serde_json::Value) -> Option<&Vec<serde_json::Value>> {
if let Some(items) = value.as_array() {
return Some(items);
}
first_json_value_by_paths(
value,
&[
@@ -799,6 +803,10 @@ fn codex_reset_credit_detail_items(value: &serde_json::Value) -> Option<&Vec<ser
&["rateLimitResetCredits", "data"],
&["reset_credits", "credits"],
&["resetCredits", "credits"],
&["rate_limit_reset_credits"],
&["rateLimitResetCredits"],
&["reset_credits"],
&["resetCredits"],
],
)
.and_then(serde_json::Value::as_array)
@@ -833,10 +841,28 @@ fn codex_reset_credit_status(
.find_map(|key| coerce_json_string(object.get(*key)))
}
fn codex_reset_credit_is_available(object: &serde_json::Map<String, serde_json::Value>) -> bool {
let reset_type = ["reset_type", "resetType"]
.iter()
.find_map(|key| coerce_json_string(object.get(*key)));
if reset_type
.as_deref()
.is_some_and(|value| !value.trim().eq_ignore_ascii_case("codex_rate_limits"))
{
return false;
}
codex_reset_credit_status(object).is_none_or(|status| {
let status = status.trim();
status.eq_ignore_ascii_case("available") || status.eq_ignore_ascii_case("active")
})
}
fn parse_codex_reset_credit_detail_item(item: &serde_json::Value) -> Option<serde_json::Value> {
let object = item.as_object()?;
let id = codex_reset_credit_id(object)?;
let display_key = codex_reset_credit_display_key(&id)?;
if !codex_reset_credit_is_available(object) {
return None;
}
let expires_at = parse_codex_reset_credit_timestamp(
object
.get("expires_at")
@@ -853,8 +879,12 @@ fn parse_codex_reset_credit_detail_item(item: &serde_json::Value) -> Option<serd
);
let mut out = serde_json::Map::new();
out.insert("id".to_string(), json!(id));
out.insert("display_key".to_string(), json!(display_key));
if let Some(id) = codex_reset_credit_id(object) {
if let Some(display_key) = codex_reset_credit_display_key(&id) {
out.insert("display_key".to_string(), json!(display_key));
}
out.insert("id".to_string(), json!(id));
}
if let Some(status) = codex_reset_credit_status(object) {
out.insert("status".to_string(), json!(status));
}
@@ -869,8 +899,35 @@ pub fn parse_codex_wham_reset_credits_detail_response(
value: &serde_json::Value,
updated_at_unix_secs: u64,
) -> Option<serde_json::Value> {
value.as_object()?;
let mut credits = codex_reset_credit_detail_items(value)
let root = value.as_object();
let detail_items = codex_reset_credit_detail_items(value);
if root.is_none() && detail_items.is_none() {
return None;
}
let available_item_count = detail_items
.into_iter()
.flatten()
.filter_map(serde_json::Value::as_object)
.filter(|item| codex_reset_credit_is_available(item))
.count();
let available_count = root
.and_then(codex_reset_credits_available_count)
.or_else(|| {
root.and_then(|root| {
[
"available_count",
"availableCount",
"available",
"remaining",
"count",
]
.iter()
.find_map(|key| root.get(*key).and_then(coerce_json_u64))
})
})
.or_else(|| detail_items.and_then(|_| u64::try_from(available_item_count).ok()));
let mut credits = detail_items
.into_iter()
.flatten()
.filter_map(parse_codex_reset_credit_detail_item)
@@ -881,10 +938,10 @@ pub fn parse_codex_wham_reset_credits_detail_response(
.unwrap_or(u64::MAX)
});
let detail_status = if credits.is_empty() {
"empty"
} else {
let detail_status = if available_count.is_some_and(|count| count > 0) {
"available"
} else {
"empty"
};
let mut reset_credits = serde_json::Map::new();
reset_credits.insert("updated_at".to_string(), json!(updated_at_unix_secs));
@@ -892,20 +949,8 @@ pub fn parse_codex_wham_reset_credits_detail_response(
reset_credits.insert("detail_status".to_string(), json!(detail_status));
reset_credits.insert("credits".to_string(), serde_json::Value::Array(credits));
if let Some(root) = value.as_object() {
if let Some(available_count) = codex_reset_credits_available_count(root).or_else(|| {
[
"available_count",
"availableCount",
"available",
"remaining",
"count",
]
.iter()
.find_map(|key| root.get(*key).and_then(coerce_json_u64))
}) {
reset_credits.insert("available_count".to_string(), json!(available_count));
}
if let Some(available_count) = available_count {
reset_credits.insert("available_count".to_string(), json!(available_count));
}
Some(json!({ "reset_credits": reset_credits }))
@@ -2534,6 +2579,61 @@ mod tests {
);
}
#[test]
fn parses_codex_reset_credit_detail_without_explicit_count_or_ids() {
let parsed = parse_codex_wham_reset_credits_detail_response(
&json!({
"rate_limit_reset_credits": [
{
"resetType": "codex_rate_limits",
"status": "available",
"expiresAt": "2030-01-02T00:00:00Z"
},
{
"reset_type": "codex_rate_limits",
"status": "available"
},
{
"reset_type": "codex_rate_limits",
"status": "redeemed",
"expires_at": "2030-01-03T00:00:00Z"
}
]
}),
1_777_000_000,
)
.expect("detail array should parse");
assert_eq!(
parsed.pointer("/reset_credits/available_count"),
Some(&json!(2u64))
);
assert_eq!(
parsed.pointer("/reset_credits/credits/0/expires_at"),
Some(&json!(1_893_542_400u64))
);
assert_eq!(parsed.pointer("/reset_credits/credits/0/id"), None);
}
#[test]
fn parses_codex_reset_credit_detail_from_top_level_array() {
let parsed = parse_codex_wham_reset_credits_detail_response(
&json!([
{
"status": "available",
"expires_at": "2030-01-04T00:00:00Z"
}
]),
1_777_000_000,
)
.expect("top-level detail array should parse");
assert_eq!(
parsed.pointer("/reset_credits/available_count"),
Some(&json!(1u64))
);
}
#[test]
fn normalizes_codex_reset_credit_consume_outcome() {
assert_eq!(