mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(accounts): consume reset credits without card details (#81)
Allow count-only reset inventories to request upstream card selection without a credit ID. Preserve confirmation and idempotent retry guards, and place the fallback action in the shared modal footer with balanced inventory spacing. Co-authored-by: Yanshan Chen <16632048+chenyanshan@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
co-authored by
Yanshan Chen
Codex
parent
d8bc91efce
commit
c9be17778d
@@ -146,6 +146,50 @@ async fn consume_should_send_exact_credit_and_redeem_request_id_without_retry()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn count_only_credits_should_allow_upstream_selected_consumption() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/api/codex/rate-limit-reset-credits"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"available_count": 1,
|
||||
"credits": []
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/api/codex/rate-limit-reset-credits/consume"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "code": "reset" })))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let client = client(&server.uri());
|
||||
let credits = client
|
||||
.list_rate_limit_reset_credits(context())
|
||||
.await
|
||||
.expect("count-only reset credits");
|
||||
assert_eq!(credits.available_count, 1);
|
||||
assert!(credits.credits.is_empty());
|
||||
|
||||
let redeem_request_id =
|
||||
Uuid::parse_str("8fbf302d-11df-4bd5-82e4-08e4b3df7874").expect("redeem request ID");
|
||||
let result = client
|
||||
.consume_rate_limit_reset_credit(context(), None, redeem_request_id)
|
||||
.await
|
||||
.expect("upstream-selected reset-credit consume");
|
||||
assert_eq!(result.code, "reset");
|
||||
let requests = server.received_requests().await.expect("received requests");
|
||||
let request = requests
|
||||
.iter()
|
||||
.find(|request| request.method == Method::POST)
|
||||
.expect("consume request");
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<serde_json::Value>(&request.body).expect("consume JSON"),
|
||||
json!({ "redeem_request_id": "8fbf302d-11df-4bd5-82e4-08e4b3df7874" })
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn consume_should_preserve_raw_upstream_error_and_never_retry() {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
@@ -60,12 +60,18 @@ const triggerLabel = computed(() => {
|
||||
return hasSnapshot.value ? `查看主动重置卡,最近查询 ${availableCount.value} 张可用` : '查看主动重置卡'
|
||||
})
|
||||
const showTriggerCount = computed(() => hasSnapshot.value && availableCount.value > 0)
|
||||
const confirmCreditTitle = computed(() => creditTitle(consumptionCredit.value))
|
||||
const confirmCreditTitle = computed(() => consumptionCredit.value
|
||||
? creditTitle(consumptionCredit.value)
|
||||
: '使用一次重置(由上游选择)')
|
||||
const creditItems = computed(() => availableCredits.value.map(credit => ({
|
||||
id: credit.id,
|
||||
title: creditTitle(credit),
|
||||
expiry: expiryLabel(credit.expiresAt),
|
||||
})))
|
||||
const showCountOnlyAction = computed(() => !loadError.value
|
||||
&& hasSnapshot.value
|
||||
&& availableCount.value > 0
|
||||
&& creditItems.value.length === 0)
|
||||
const countLabel = computed(() => {
|
||||
if (!hasSnapshot.value)
|
||||
return loading.value ? '查询中' : '待查询'
|
||||
@@ -159,7 +165,10 @@ function handleRequestConsume(creditId: string) {
|
||||
</section>
|
||||
|
||||
<section class="overflow-hidden rounded-cp bg-cp-fill-quaternary" aria-label="使用限额重置">
|
||||
<div class="flex items-center gap-3 pt-4 pr-3 pb-1 pl-4">
|
||||
<div
|
||||
class="flex items-center gap-3 pt-4 pr-3 pl-4"
|
||||
:class="showCountOnlyAction ? 'pb-4' : 'pb-1'"
|
||||
>
|
||||
<h3 class="m-0 min-w-0 flex-1 text-cp-sm font-heavy text-cp-text">
|
||||
使用限额重置
|
||||
</h3>
|
||||
@@ -227,7 +236,7 @@ function handleRequestConsume(creditId: string) {
|
||||
</ul>
|
||||
|
||||
<BaseEmpty
|
||||
v-else
|
||||
v-else-if="!showCountOnlyAction"
|
||||
:icon="TicketCheck"
|
||||
size="sm"
|
||||
surface="none"
|
||||
@@ -238,12 +247,23 @@ function handleRequestConsume(creditId: string) {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template v-if="showConfirm" #footer>
|
||||
<BaseButton variant="secondary" :disabled="consuming" @click="cancelConsume">
|
||||
返回
|
||||
</BaseButton>
|
||||
<BaseButton variant="primary" :loading="consuming" @click="confirmConsume">
|
||||
{{ ambiguous ? '再次确认' : '确认重置' }}
|
||||
<template v-if="showConfirm || showCountOnlyAction" #footer>
|
||||
<template v-if="showConfirm">
|
||||
<BaseButton variant="secondary" :disabled="consuming" @click="cancelConsume">
|
||||
返回
|
||||
</BaseButton>
|
||||
<BaseButton variant="primary" :loading="consuming" :disabled="loading || !canRequestConsume" @click="confirmConsume">
|
||||
{{ ambiguous ? '再次确认' : '确认重置' }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
<BaseButton
|
||||
v-else
|
||||
size="sm"
|
||||
variant="primary"
|
||||
:disabled="loading || consuming || ambiguous || !canRequestConsume"
|
||||
@click="requestConsume"
|
||||
>
|
||||
使用一次重置(由上游选择)
|
||||
</BaseButton>
|
||||
</template>
|
||||
</BaseModal>
|
||||
|
||||
@@ -12,8 +12,8 @@ import { errorMessage } from '@/utils/async'
|
||||
|
||||
interface PendingResetCreditOperation {
|
||||
accountId: string
|
||||
creditId: string
|
||||
credit: AccountResetCredit
|
||||
creditId?: string
|
||||
credit?: AccountResetCredit
|
||||
redeemRequestId: string
|
||||
hasTransportFailure: boolean
|
||||
}
|
||||
@@ -104,17 +104,21 @@ export function useAccountResetCredits(options: {
|
||||
availableCredits.value.find(credit => credit.id === selectedCreditId.value),
|
||||
)
|
||||
const consumptionCredit = computed(() =>
|
||||
pendingOperation.value?.credit ?? selectedCredit.value,
|
||||
pendingOperation.value ? pendingOperation.value.credit : selectedCredit.value,
|
||||
)
|
||||
const ambiguous = computed(() => pendingOperation.value?.hasTransportFailure === true)
|
||||
const canStartConsume = computed(() =>
|
||||
hasSnapshot.value && !loadError.value && availableCount.value > 0
|
||||
&& (selectedCredit.value !== undefined || availableCredits.value.length === 0),
|
||||
)
|
||||
const canRequestConsume = computed(() =>
|
||||
ambiguous.value || (availableCount.value > 0 && selectedCredit.value !== undefined),
|
||||
ambiguous.value || canStartConsume.value,
|
||||
)
|
||||
|
||||
function reconcileSelectedCredit() {
|
||||
const operation = pendingOperation.value
|
||||
if (operation) {
|
||||
selectedCreditId.value = operation.creditId
|
||||
selectedCreditId.value = operation.creditId ?? ''
|
||||
return
|
||||
}
|
||||
|
||||
@@ -163,10 +167,10 @@ export function useAccountResetCredits(options: {
|
||||
|
||||
const credit = selectedCredit.value
|
||||
// 确认发送时才建立操作;仅打开或取消确认弹窗不占用账号的消费状态。
|
||||
const operation = target.pendingOperation ?? (credit && availableCount.value > 0
|
||||
const operation = target.pendingOperation ?? (canStartConsume.value
|
||||
? {
|
||||
accountId: target.accountId,
|
||||
creditId: credit.id,
|
||||
creditId: credit?.id,
|
||||
credit,
|
||||
redeemRequestId: generateRedeemRequestId(),
|
||||
hasTransportFailure: false,
|
||||
|
||||
Reference in New Issue
Block a user