mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
feat(accounts): add confirmed Claude reset button next to reset count
Orange 「重置」 button mirrors the Codex cell: enabled only after a query shows a redeemable credit, danger confirm, one idempotency key per confirmation reused on retry, outcome-specific messages, then refreshes credits and the usage row. Ported from upstream PR #7591. Co-authored-by: korkin25 <eugeny.kuzakov@gmail.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
korkin25
Claude Opus 5.5
parent
196b47757a
commit
1872d44b58
@@ -25,3 +25,23 @@ export async function getClaudeResetCredits(id: number): Promise<ClaudeResetCred
|
||||
const { data } = await apiClient.get<ClaudeResetCredits>(`/admin/accounts/${id}/claude/reset-credits`)
|
||||
return data
|
||||
}
|
||||
|
||||
export type ClaudeResetOutcomeKind = 'reset' | 'already_used' | 'not_limited' | 'cooldown' | 'ineligible' | 'unknown'
|
||||
|
||||
export interface ClaudeResetOutcome {
|
||||
outcome: ClaudeResetOutcomeKind
|
||||
reason?: string
|
||||
cleared?: string[]
|
||||
cooldown_until?: string
|
||||
credits?: ClaudeResetCredits
|
||||
replayed: boolean
|
||||
}
|
||||
|
||||
// Consumes one reset credit (irreversible). The server picks the grant; the key
|
||||
// identifies one operator confirmation and must be reused when retrying it.
|
||||
export async function redeemClaudeResetCredit(id: number, idempotencyKey: string): Promise<ClaudeResetOutcome> {
|
||||
const { data } = await apiClient.post<ClaudeResetOutcome>(`/admin/accounts/${id}/claude/reset-credits/redeem`, undefined, {
|
||||
headers: { 'Idempotency-Key': idempotencyKey }
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
while usage is still loading survives the usage response. The local query
|
||||
button shares its row once usage data exists.
|
||||
-->
|
||||
<ClaudeResetCreditsCell :account="account" class="mt-1">
|
||||
<ClaudeResetCreditsCell :account="account" class="mt-1" @redeemed="loadActiveUsage">
|
||||
<template v-if="usageInfo" #pre-actions>
|
||||
<span
|
||||
v-if="usageInfo.source === 'passive'"
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
Same action row layout as OpenAIQuotaResetCell: the parent's local
|
||||
"查询" button is passed in via #pre-actions so related buttons share one
|
||||
row. The reset count only shows for Anthropic OAuth accounts; the slot always
|
||||
renders. This cell is read-only — it never redeems a reset.
|
||||
renders. The count button is read-only; only the orange reset button,
|
||||
after a query shows a redeemable credit and the operator confirms,
|
||||
consumes one reset.
|
||||
-->
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<slot name="pre-actions" />
|
||||
@@ -14,7 +16,7 @@
|
||||
type="button"
|
||||
data-testid="claude-reset-count"
|
||||
class="inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 text-[10px] font-medium text-blue-600 transition-colors hover:bg-blue-50 disabled:cursor-not-allowed disabled:opacity-50 dark:text-blue-400 dark:hover:bg-blue-900/30"
|
||||
:disabled="loading"
|
||||
:disabled="loading || redeeming"
|
||||
:title="countButtonTitle"
|
||||
@click="refresh"
|
||||
>
|
||||
@@ -34,6 +36,32 @@
|
||||
</svg>
|
||||
{{ t('admin.accounts.claudeResetCredits.count') }}<span v-if="status" class="ml-0.5 tabular-nums">{{ totalResets }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="visible"
|
||||
type="button"
|
||||
data-testid="claude-reset-redeem"
|
||||
class="inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 text-[10px] font-medium text-orange-600 transition-colors hover:bg-orange-50 disabled:cursor-not-allowed disabled:opacity-50 dark:text-orange-400 dark:hover:bg-orange-900/30"
|
||||
:disabled="redeeming || loading || !canRedeem"
|
||||
:title="redeemButtonTitle"
|
||||
@click="openRedeemConfirm"
|
||||
>
|
||||
<svg
|
||||
class="h-2.5 w-2.5"
|
||||
:class="{ 'animate-spin': redeeming }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M20 12a8 8 0 11-2.343-5.657L20 8m0 0V4m0 4h-4"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('admin.accounts.claudeResetCredits.reset') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="visible && status && (primaryCredit || !status.eligible || cooldownActive)" class="flex flex-wrap items-center gap-1">
|
||||
@@ -63,6 +91,28 @@
|
||||
<div v-if="visible && error" role="alert" class="text-[10px] text-red-600 dark:text-red-400">
|
||||
{{ t('admin.accounts.claudeResetCredits.error') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="visible && redeemFeedback"
|
||||
data-testid="claude-reset-feedback"
|
||||
:role="redeemFeedback.kind === 'success' ? 'status' : 'alert'"
|
||||
class="text-[10px]"
|
||||
:class="feedbackClass"
|
||||
:title="redeemFeedback.text"
|
||||
>
|
||||
{{ redeemFeedback.text }}
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-if="visible"
|
||||
:show="showRedeemConfirm"
|
||||
:title="t('admin.accounts.claudeResetCredits.confirmTitle')"
|
||||
:message="confirmMessage"
|
||||
:confirm-text="t('admin.accounts.claudeResetCredits.reset')"
|
||||
:cancel-text="t('common.cancel')"
|
||||
danger
|
||||
@confirm="confirmRedeem"
|
||||
@cancel="showRedeemConfirm = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -70,18 +120,41 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { Account } from '@/types'
|
||||
import { getClaudeResetCredits, type ClaudeResetCredits } from '@/api/admin/claudeResetCredits'
|
||||
import {
|
||||
getClaudeResetCredits,
|
||||
redeemClaudeResetCredit,
|
||||
type ClaudeResetCredits,
|
||||
type ClaudeResetOutcome
|
||||
} from '@/api/admin/claudeResetCredits'
|
||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||
|
||||
const props = defineProps<{ account: Account }>()
|
||||
// Fired after a redemption attempt so the parent can refresh the usage row.
|
||||
const emit = defineEmits<{ redeemed: [outcome: ClaudeResetOutcome] }>()
|
||||
const { t } = useI18n()
|
||||
const status = ref<ClaudeResetCredits | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
const redeeming = ref(false)
|
||||
const showRedeemConfirm = ref(false)
|
||||
const redeemFeedback = ref<{ kind: 'success' | 'warning' | 'error'; text: string } | null>(null)
|
||||
// One key per operator confirmation. It survives a failed request so retrying the
|
||||
// same confirmation replays server-side instead of claiming a second reset.
|
||||
let pendingKey: string | null = null
|
||||
let generation = 0
|
||||
|
||||
const visible = computed(() => props.account.platform === 'anthropic' && props.account.type === 'oauth')
|
||||
|
||||
watch(() => [props.account.id, props.account.platform, props.account.type], () => { generation++; status.value = null; loading.value = false; error.value = false })
|
||||
watch(() => [props.account.id, props.account.platform, props.account.type], () => {
|
||||
generation++
|
||||
status.value = null
|
||||
loading.value = false
|
||||
error.value = false
|
||||
redeeming.value = false
|
||||
showRedeemConfirm.value = false
|
||||
redeemFeedback.value = null
|
||||
pendingKey = null
|
||||
})
|
||||
|
||||
// 与 OpenAIQuotaResetCell 的到期时间格式保持一致
|
||||
const formatTime = (value: string, style: 'short' | 'full'): string => {
|
||||
@@ -131,8 +204,100 @@ const countButtonTitle = computed(() => {
|
||||
].join('\n')
|
||||
})
|
||||
|
||||
const canRedeem = computed(() => (status.value?.available_count ?? 0) > 0)
|
||||
|
||||
const redeemButtonTitle = computed(() => {
|
||||
if (!status.value) return t('admin.accounts.claudeResetCredits.resetTooltipNeedQuery')
|
||||
if (!canRedeem.value) return t('admin.accounts.claudeResetCredits.resetTooltipNone')
|
||||
return t('admin.accounts.claudeResetCredits.resetTooltipReady')
|
||||
})
|
||||
|
||||
const confirmMessage = computed(() => {
|
||||
const next = status.value?.credits.find(c => c.redeemable)
|
||||
return t('admin.accounts.claudeResetCredits.confirmMessage', {
|
||||
windows: next?.clears.join(', ') || '—',
|
||||
count: totalResets.value
|
||||
})
|
||||
})
|
||||
|
||||
const feedbackClass = computed(() => {
|
||||
switch (redeemFeedback.value?.kind) {
|
||||
case 'success': return 'text-emerald-600 dark:text-emerald-400'
|
||||
case 'warning': return 'text-amber-600 dark:text-amber-400'
|
||||
default: return 'text-red-600 dark:text-red-400'
|
||||
}
|
||||
})
|
||||
|
||||
function newOperationKey(accountID: number): string {
|
||||
const id = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
return `claude-reset-${accountID}-${id}`
|
||||
}
|
||||
|
||||
function openRedeemConfirm() {
|
||||
if (redeeming.value || loading.value || !canRedeem.value) return
|
||||
showRedeemConfirm.value = true
|
||||
}
|
||||
|
||||
function outcomeFeedback(result: ClaudeResetOutcome): { kind: 'success' | 'warning' | 'error'; text: string } {
|
||||
const key = 'admin.accounts.claudeResetCredits.outcome'
|
||||
switch (result.outcome) {
|
||||
case 'reset':
|
||||
return { kind: 'success', text: t(`${key}.reset`, { windows: result.cleared?.join(', ') || '—' }) }
|
||||
case 'already_used':
|
||||
return { kind: 'warning', text: t(`${key}.alreadyUsed`) }
|
||||
case 'cooldown':
|
||||
return {
|
||||
kind: 'warning',
|
||||
text: result.cooldown_until
|
||||
? t(`${key}.cooldownUntil`, { time: formatTime(result.cooldown_until, 'short') })
|
||||
: t(`${key}.cooldown`)
|
||||
}
|
||||
case 'not_limited':
|
||||
return { kind: 'warning', text: t(`${key}.notLimited`) }
|
||||
case 'ineligible':
|
||||
return { kind: 'error', text: t(`${key}.ineligible`) }
|
||||
default:
|
||||
return { kind: 'warning', text: t(`${key}.unknown`) }
|
||||
}
|
||||
}
|
||||
|
||||
function errorText(e: unknown): string {
|
||||
const err = e as { reason?: string; message?: string }
|
||||
switch (err?.reason) {
|
||||
case 'CLAUDE_RESET_UNRESOLVED': return t('admin.accounts.claudeResetCredits.outcome.unknown')
|
||||
case 'CLAUDE_RESET_BUSY': return t('admin.accounts.claudeResetCredits.outcome.busy')
|
||||
case 'CLAUDE_RESET_NOT_AVAILABLE': return t('admin.accounts.claudeResetCredits.outcome.notAvailable')
|
||||
default: return err?.message || t('admin.accounts.claudeResetCredits.outcome.failed')
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmRedeem() {
|
||||
showRedeemConfirm.value = false
|
||||
if (redeeming.value || loading.value || !canRedeem.value) return
|
||||
const accountID = props.account.id
|
||||
const current = generation
|
||||
pendingKey ??= newOperationKey(accountID)
|
||||
redeeming.value = true
|
||||
redeemFeedback.value = null
|
||||
try {
|
||||
const result = await redeemClaudeResetCredit(accountID, pendingKey)
|
||||
if (current !== generation) return
|
||||
// A definite server answer ends this confirmation; the next one gets a new key.
|
||||
pendingKey = null
|
||||
redeemFeedback.value = outcomeFeedback(result)
|
||||
redeeming.value = false
|
||||
emit('redeemed', result)
|
||||
await refresh()
|
||||
} catch (e) {
|
||||
if (current !== generation) return
|
||||
redeemFeedback.value = { kind: 'error', text: errorText(e) }
|
||||
} finally {
|
||||
if (current === generation) redeeming.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
if (loading.value) return
|
||||
if (loading.value || redeeming.value) return
|
||||
const current = ++generation
|
||||
loading.value = true
|
||||
error.value = false
|
||||
|
||||
@@ -3,8 +3,19 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ClaudeResetCreditsCell from '../ClaudeResetCreditsCell.vue'
|
||||
import type { Account } from '@/types'
|
||||
const getCredits = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/api/admin/claudeResetCredits', () => ({ getClaudeResetCredits: getCredits }))
|
||||
const redeem = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/api/admin/claudeResetCredits', () => ({ getClaudeResetCredits: getCredits, redeemClaudeResetCredit: redeem }))
|
||||
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) }))
|
||||
// Minimal stand-in exposing the dialog's show state and confirm/cancel events.
|
||||
vi.mock('@/components/common/ConfirmDialog.vue', () => ({
|
||||
default: {
|
||||
props: { show: Boolean, title: String, message: String, confirmText: String, cancelText: String, danger: Boolean },
|
||||
emits: ['confirm', 'cancel'],
|
||||
template: `<div v-if="show" data-testid="confirm-dialog" :data-danger="String(danger)">{{ message }}
|
||||
<button data-testid="confirm-ok" @click="$emit('confirm')">ok</button>
|
||||
<button data-testid="confirm-cancel" @click="$emit('cancel')">cancel</button></div>`
|
||||
}
|
||||
}))
|
||||
const account = { id: 1, platform: 'anthropic', type: 'oauth' } as Account
|
||||
const credit = {
|
||||
label: 'Launch reset', resets_left: 1,
|
||||
@@ -13,9 +24,22 @@ const credit = {
|
||||
}
|
||||
const snapshot = { eligible: true, available_count: 1, credits: [credit], fetched_at: '2026-09-25T00:00:00Z' }
|
||||
const countButton = (wrapper: ReturnType<typeof mount>) => wrapper.find('[data-testid="claude-reset-count"]')
|
||||
const redeemButton = (wrapper: ReturnType<typeof mount>) => wrapper.get('[data-testid="claude-reset-redeem"]')
|
||||
async function queried(result: unknown = snapshot) {
|
||||
getCredits.mockResolvedValue(result)
|
||||
const wrapper = mount(ClaudeResetCreditsCell, { props: { account } })
|
||||
await countButton(wrapper).trigger('click')
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
async function confirmReset(wrapper: ReturnType<typeof mount>) {
|
||||
await redeemButton(wrapper).trigger('click')
|
||||
await wrapper.get('[data-testid="confirm-ok"]').trigger('click')
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
describe('Claude reset credit status', () => {
|
||||
beforeEach(() => getCredits.mockReset())
|
||||
beforeEach(() => { getCredits.mockReset(); redeem.mockReset() })
|
||||
|
||||
it('queries only on explicit request and shows count and expiry', async () => {
|
||||
getCredits.mockResolvedValue(snapshot)
|
||||
@@ -52,9 +76,89 @@ describe('Claude reset credit status', () => {
|
||||
expect(wrapper.find('[data-testid="claude-reset-not-usable"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('offers no redeem action', () => {
|
||||
it('disables reset until a query shows a redeemable credit', async () => {
|
||||
const wrapper = mount(ClaudeResetCreditsCell, { props: { account } })
|
||||
expect(wrapper.findAll('button')).toHaveLength(1)
|
||||
expect(redeemButton(wrapper).attributes('disabled')).toBeDefined()
|
||||
await redeemButton(wrapper).trigger('click')
|
||||
expect(wrapper.find('[data-testid="confirm-dialog"]').exists()).toBe(false)
|
||||
|
||||
const waiting = await queried({ ...snapshot, available_count: 0, credits: [{ ...credit, redeemable: false }] })
|
||||
expect(redeemButton(waiting).attributes('disabled')).toBeDefined()
|
||||
|
||||
let resolve!: (value: typeof snapshot) => void
|
||||
getCredits.mockReturnValue(new Promise(r => { resolve = r }))
|
||||
const loading = mount(ClaudeResetCreditsCell, { props: { account } })
|
||||
await countButton(loading).trigger('click')
|
||||
expect(redeemButton(loading).attributes('disabled')).toBeDefined()
|
||||
resolve(snapshot)
|
||||
await flushPromises()
|
||||
expect(redeemButton(loading).attributes('disabled')).toBeUndefined()
|
||||
expect(redeem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('never redeems without confirmation, and the count button never redeems', async () => {
|
||||
const wrapper = await queried()
|
||||
await countButton(wrapper).trigger('click')
|
||||
await flushPromises()
|
||||
await redeemButton(wrapper).trigger('click')
|
||||
const dialog = wrapper.get('[data-testid="confirm-dialog"]')
|
||||
expect(dialog.attributes('data-danger')).toBe('true')
|
||||
expect(dialog.text()).toContain('admin.accounts.claudeResetCredits.confirmMessage')
|
||||
await wrapper.get('[data-testid="confirm-cancel"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-testid="confirm-dialog"]').exists()).toBe(false)
|
||||
expect(redeem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('redeems after confirmation, reports success, refreshes and notifies the parent', async () => {
|
||||
const wrapper = await queried()
|
||||
redeem.mockResolvedValue({ outcome: 'reset', cleared: ['five_hour'], replayed: false })
|
||||
getCredits.mockResolvedValue({ ...snapshot, available_count: 0, credits: [] })
|
||||
await confirmReset(wrapper)
|
||||
expect(redeem).toHaveBeenCalledTimes(1)
|
||||
expect(redeem.mock.calls[0][0]).toBe(1)
|
||||
expect(redeem.mock.calls[0][1]).toMatch(/^claude-reset-1-/)
|
||||
expect(wrapper.get('[data-testid="claude-reset-feedback"]').text()).toBe('admin.accounts.claudeResetCredits.outcome.reset')
|
||||
expect(getCredits).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.emitted('redeemed')).toHaveLength(1)
|
||||
expect(redeemButton(wrapper).attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('reuses the key when retrying a failed confirmation and rotates it after an answer', async () => {
|
||||
const wrapper = await queried()
|
||||
redeem.mockRejectedValueOnce({ status: 0, message: 'Network Error' })
|
||||
await confirmReset(wrapper)
|
||||
expect(wrapper.get('[data-testid="claude-reset-feedback"]').text()).toBe('Network Error')
|
||||
redeem.mockResolvedValueOnce({ outcome: 'not_limited', replayed: false })
|
||||
await confirmReset(wrapper)
|
||||
expect(redeem.mock.calls[1][1]).toBe(redeem.mock.calls[0][1])
|
||||
redeem.mockResolvedValueOnce({ outcome: 'not_limited', replayed: false })
|
||||
await confirmReset(wrapper)
|
||||
expect(redeem.mock.calls[2][1]).not.toBe(redeem.mock.calls[0][1])
|
||||
})
|
||||
|
||||
it.each([
|
||||
['already_used', 'alreadyUsed'],
|
||||
['cooldown', 'cooldown'],
|
||||
['not_limited', 'notLimited'],
|
||||
['ineligible', 'ineligible'],
|
||||
['unknown', 'unknown']
|
||||
])('maps outcome %s to a clear message', async (outcome, key) => {
|
||||
const wrapper = await queried()
|
||||
redeem.mockResolvedValue({ outcome, replayed: false })
|
||||
await confirmReset(wrapper)
|
||||
expect(wrapper.get('[data-testid="claude-reset-feedback"]').text()).toBe(`admin.accounts.claudeResetCredits.outcome.${key}`)
|
||||
expect(getCredits).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('maps server refusals to clear messages', async () => {
|
||||
const wrapper = await queried()
|
||||
redeem.mockRejectedValueOnce({ status: 409, reason: 'CLAUDE_RESET_UNRESOLVED', message: 'x' })
|
||||
await confirmReset(wrapper)
|
||||
expect(wrapper.get('[data-testid="claude-reset-feedback"]').text()).toBe('admin.accounts.claudeResetCredits.outcome.unknown')
|
||||
redeem.mockRejectedValueOnce({ status: 409, reason: 'CLAUDE_RESET_BUSY', message: 'x' })
|
||||
await confirmReset(wrapper)
|
||||
expect(wrapper.get('[data-testid="claude-reset-feedback"]').text()).toBe('admin.accounts.claudeResetCredits.outcome.busy')
|
||||
})
|
||||
|
||||
it('keeps slot content for setup tokens and discards responses after account changes', async () => {
|
||||
|
||||
@@ -938,7 +938,25 @@ export default {
|
||||
expiresAtFull: 'Reset credit expires at: {time}',
|
||||
clears: 'Clears windows: {windows}',
|
||||
notUsableNow: 'Not usable now',
|
||||
requiresLimit: 'Usable only after hitting a limit'
|
||||
requiresLimit: 'Usable only after hitting a limit',
|
||||
reset: 'Reset',
|
||||
resetTooltipNeedQuery: 'Check the count first; reset is available once a usable credit is found',
|
||||
resetTooltipNone: 'No reset can be used right now',
|
||||
resetTooltipReady: 'Consume 1 reset to clear limit windows (asks for confirmation)',
|
||||
confirmTitle: 'Use a Claude reset',
|
||||
confirmMessage: 'This immediately consumes 1 reset and cannot be undone. Windows cleared: {windows}. Resets held: {count}. Continue?',
|
||||
outcome: {
|
||||
reset: 'Reset applied; cleared: {windows}',
|
||||
alreadyUsed: 'This reset was already used; refreshing to confirm',
|
||||
cooldown: 'Resets are cooling down; try again later',
|
||||
cooldownUntil: 'Resets are cooling down until {time}',
|
||||
notLimited: 'Not at a limit, so nothing was reset and no credit was used',
|
||||
ineligible: 'This account cannot use resets right now',
|
||||
unknown: 'Result unconfirmed; further redemption is blocked for now. Check again later',
|
||||
busy: 'Another reset is in progress; try again later',
|
||||
notAvailable: 'No reset can be used right now; no credit was used',
|
||||
failed: 'Reset request failed'
|
||||
}
|
||||
},
|
||||
autoResetCredit: {
|
||||
title: 'Automatically use reset credits',
|
||||
|
||||
@@ -1039,7 +1039,25 @@ export default {
|
||||
expiresAtFull: '重置次数到期时间:{time}',
|
||||
clears: '可清除窗口:{windows}',
|
||||
notUsableNow: '暂不可用',
|
||||
requiresLimit: '需达到限额后才能使用'
|
||||
requiresLimit: '需达到限额后才能使用',
|
||||
reset: '重置',
|
||||
resetTooltipNeedQuery: '请先点「次数」查询;查询到可用的重置后才能使用',
|
||||
resetTooltipNone: '当前没有可立即使用的重置',
|
||||
resetTooltipReady: '消耗 1 次重置,清除限额窗口(需确认)',
|
||||
confirmTitle: '使用 Claude 重置',
|
||||
confirmMessage: '将立即消耗 1 次重置,此操作不可撤销。将清除窗口:{windows}。当前持有 {count} 次。确定继续?',
|
||||
outcome: {
|
||||
reset: '重置成功,已清除:{windows}',
|
||||
alreadyUsed: '该重置已被使用,正在刷新确认',
|
||||
cooldown: '重置处于冷却中,请稍后再试',
|
||||
cooldownUntil: '重置处于冷却中,冷却至 {time}',
|
||||
notLimited: '当前未达到限额,无需重置,未消耗次数',
|
||||
ineligible: '此账号当前不可使用重置',
|
||||
unknown: '结果未确认,已阻止再次兑换,请稍后查询',
|
||||
busy: '另一个重置正在进行中,请稍后再试',
|
||||
notAvailable: '当前没有可立即使用的重置,未消耗次数',
|
||||
failed: '重置请求失败'
|
||||
}
|
||||
},
|
||||
autoResetCredit: {
|
||||
title: '自动使用重置卡',
|
||||
|
||||
Reference in New Issue
Block a user