From 9cafa46dd36ff1fea18202fb1e7bafa6dffd3668 Mon Sep 17 00:00:00 2001 From: yangjianbo Date: Sat, 14 Feb 2026 12:06:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(accounts):=20=E8=B4=A6=E5=8F=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=94=B9=E4=B8=BA=E5=8D=95=E8=A1=8C=E5=A2=9E=E9=87=8F?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=B9=B6=E9=81=BF=E5=85=8D=E5=85=A8=E9=87=8F?= =?UTF-8?q?=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将编辑与重新授权成功事件改为回传更新后的账号对象 - 在账号列表页按 id 就地补丁更新单行数据并保留运行时容量字段 - 单账号操作(刷新凭证/清错/清限流/临时不可调度重置)改为单行更新 - 后端增强 clear-rate-limit 接口,返回更新后的账号对象 - 同步前端 clearRateLimit API 类型定义 Co-Authored-By: Claude Opus 4.6 --- .../internal/handler/admin/account_handler.go | 8 +- frontend/src/api/admin/accounts.ts | 6 +- .../components/account/EditAccountModal.vue | 10 +- .../admin/account/ReAuthAccountModal.vue | 22 ++--- frontend/src/views/admin/AccountsView.vue | 92 +++++++++++++++++-- 5 files changed, 112 insertions(+), 26 deletions(-) diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index c031d6d6..fb2c5b2a 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -1106,7 +1106,13 @@ func (h *AccountHandler) ClearRateLimit(c *gin.Context) { return } - response.Success(c, gin.H{"message": "Rate limit cleared successfully"}) + account, err := h.adminService.GetAccount(c.Request.Context(), accountID) + if err != nil { + response.ErrorFrom(c, err) + return + } + + response.Success(c, dto.AccountFromService(account)) } // GetTempUnschedulable handles getting temporary unschedulable status diff --git a/frontend/src/api/admin/accounts.ts b/frontend/src/api/admin/accounts.ts index 4cb1a6f2..65f2090c 100644 --- a/frontend/src/api/admin/accounts.ts +++ b/frontend/src/api/admin/accounts.ts @@ -164,10 +164,10 @@ export async function getUsage(id: number): Promise { /** * Clear account rate limit status * @param id - Account ID - * @returns Success confirmation + * @returns Updated account */ -export async function clearRateLimit(id: number): Promise<{ message: string }> { - const { data } = await apiClient.post<{ message: string }>( +export async function clearRateLimit(id: number): Promise { + const { data } = await apiClient.post( `/admin/accounts/${id}/clear-rate-limit` ) return data diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 32b8d5a9..0b6d00c9 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -1111,7 +1111,7 @@ interface Props { const props = defineProps() const emit = defineEmits<{ close: [] - updated: [] + updated: [account: Account] }>() const { t } = useI18n() @@ -1849,9 +1849,9 @@ const handleSubmit = async () => { updatePayload.extra = newExtra } - await adminAPI.accounts.update(props.account.id, updatePayload) + const updatedAccount = await adminAPI.accounts.update(props.account.id, updatePayload) appStore.showSuccess(t('admin.accounts.accountUpdated')) - emit('updated') + emit('updated', updatedAccount) handleClose() } catch (error: any) { // Handle 409 mixed_channel_warning - show confirmation dialog @@ -1879,9 +1879,9 @@ const handleMixedChannelConfirm = async () => { pendingUpdatePayload.value.confirm_mixed_channel_risk = true submitting.value = true try { - await adminAPI.accounts.update(props.account.id, pendingUpdatePayload.value) + const updatedAccount = await adminAPI.accounts.update(props.account.id, pendingUpdatePayload.value) appStore.showSuccess(t('admin.accounts.accountUpdated')) - emit('updated') + emit('updated', updatedAccount) handleClose() } catch (error: any) { appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate')) diff --git a/frontend/src/components/admin/account/ReAuthAccountModal.vue b/frontend/src/components/admin/account/ReAuthAccountModal.vue index 8133e029..eeb3f288 100644 --- a/frontend/src/components/admin/account/ReAuthAccountModal.vue +++ b/frontend/src/components/admin/account/ReAuthAccountModal.vue @@ -216,7 +216,7 @@ interface Props { const props = defineProps() const emit = defineEmits<{ close: [] - reauthorized: [] + reauthorized: [account: Account] }>() const appStore = useAppStore() @@ -370,10 +370,10 @@ const handleExchangeCode = async () => { }) // Clear error status after successful re-authorization - await adminAPI.accounts.clearError(props.account.id) + const updatedAccount = await adminAPI.accounts.clearError(props.account.id) appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) - emit('reauthorized') + emit('reauthorized', updatedAccount) handleClose() } catch (error: any) { openaiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') @@ -404,9 +404,9 @@ const handleExchangeCode = async () => { type: 'oauth', credentials }) - await adminAPI.accounts.clearError(props.account.id) + const updatedAccount = await adminAPI.accounts.clearError(props.account.id) appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) - emit('reauthorized') + emit('reauthorized', updatedAccount) handleClose() } catch (error: any) { geminiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') @@ -436,9 +436,9 @@ const handleExchangeCode = async () => { type: 'oauth', credentials }) - await adminAPI.accounts.clearError(props.account.id) + const updatedAccount = await adminAPI.accounts.clearError(props.account.id) appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) - emit('reauthorized') + emit('reauthorized', updatedAccount) handleClose() } catch (error: any) { antigravityOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') @@ -475,10 +475,10 @@ const handleExchangeCode = async () => { }) // Clear error status after successful re-authorization - await adminAPI.accounts.clearError(props.account.id) + const updatedAccount = await adminAPI.accounts.clearError(props.account.id) appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) - emit('reauthorized') + emit('reauthorized', updatedAccount) handleClose() } catch (error: any) { claudeOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') @@ -518,10 +518,10 @@ const handleCookieAuth = async (sessionKey: string) => { }) // Clear error status after successful re-authorization - await adminAPI.accounts.clearError(props.account.id) + const updatedAccount = await adminAPI.accounts.clearError(props.account.id) appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) - emit('reauthorized') + emit('reauthorized', updatedAccount) handleClose() } catch (error: any) { claudeOAuth.error.value = diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 456fc8d9..a146130e 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -239,8 +239,8 @@ - - + + @@ -694,6 +694,53 @@ const handleBulkToggleSchedulable = async (schedulable: boolean) => { } const handleBulkUpdated = () => { showBulkEdit.value = false; selIds.value = []; reload() } const handleDataImported = () => { showImportData.value = false; reload() } +const accountMatchesCurrentFilters = (account: Account) => { + if (params.platform && account.platform !== params.platform) return false + if (params.type && account.type !== params.type) return false + if (params.status) { + if (params.status === 'rate_limited') { + if (!account.rate_limit_reset_at) return false + const resetAt = new Date(account.rate_limit_reset_at).getTime() + if (!Number.isFinite(resetAt) || resetAt <= Date.now()) return false + } else if (account.status !== params.status) { + return false + } + } + const search = String(params.search || '').trim().toLowerCase() + if (search && !account.name.toLowerCase().includes(search)) return false + return true +} +const mergeRuntimeFields = (oldAccount: Account, updatedAccount: Account): Account => ({ + ...updatedAccount, + current_concurrency: updatedAccount.current_concurrency ?? oldAccount.current_concurrency, + current_window_cost: updatedAccount.current_window_cost ?? oldAccount.current_window_cost, + active_sessions: updatedAccount.active_sessions ?? oldAccount.active_sessions +}) +const patchAccountInList = (updatedAccount: Account) => { + const index = accounts.value.findIndex(account => account.id === updatedAccount.id) + if (index === -1) return + const mergedAccount = mergeRuntimeFields(accounts.value[index], updatedAccount) + if (!accountMatchesCurrentFilters(mergedAccount)) { + accounts.value = accounts.value.filter(account => account.id !== mergedAccount.id) + selIds.value = selIds.value.filter(id => id !== mergedAccount.id) + if (menu.acc?.id === mergedAccount.id) { + menu.show = false + menu.acc = null + } + return + } + const nextAccounts = [...accounts.value] + nextAccounts[index] = mergedAccount + accounts.value = nextAccounts + if (edAcc.value?.id === mergedAccount.id) edAcc.value = mergedAccount + if (reAuthAcc.value?.id === mergedAccount.id) reAuthAcc.value = mergedAccount + if (tempUnschedAcc.value?.id === mergedAccount.id) tempUnschedAcc.value = mergedAccount + if (deletingAcc.value?.id === mergedAccount.id) deletingAcc.value = mergedAccount + if (menu.acc?.id === mergedAccount.id) menu.acc = mergedAccount +} +const handleAccountUpdated = (updatedAccount: Account) => { + patchAccountInList(updatedAccount) +} const formatExportTimestamp = () => { const now = new Date() const pad2 = (value: number) => String(value).padStart(2, '0') @@ -743,9 +790,32 @@ const closeReAuthModal = () => { showReAuth.value = false; reAuthAcc.value = nul const handleTest = (a: Account) => { testingAcc.value = a; showTest.value = true } const handleViewStats = (a: Account) => { statsAcc.value = a; showStats.value = true } const handleReAuth = (a: Account) => { reAuthAcc.value = a; showReAuth.value = true } -const handleRefresh = async (a: Account) => { try { await adminAPI.accounts.refreshCredentials(a.id); load() } catch (error) { console.error('Failed to refresh credentials:', error) } } -const handleResetStatus = async (a: Account) => { try { await adminAPI.accounts.clearError(a.id); appStore.showSuccess(t('common.success')); load() } catch (error) { console.error('Failed to reset status:', error) } } -const handleClearRateLimit = async (a: Account) => { try { await adminAPI.accounts.clearRateLimit(a.id); appStore.showSuccess(t('common.success')); load() } catch (error) { console.error('Failed to clear rate limit:', error) } } +const handleRefresh = async (a: Account) => { + try { + const updated = await adminAPI.accounts.refreshCredentials(a.id) + patchAccountInList(updated) + } catch (error) { + console.error('Failed to refresh credentials:', error) + } +} +const handleResetStatus = async (a: Account) => { + try { + const updated = await adminAPI.accounts.clearError(a.id) + patchAccountInList(updated) + appStore.showSuccess(t('common.success')) + } catch (error) { + console.error('Failed to reset status:', error) + } +} +const handleClearRateLimit = async (a: Account) => { + try { + const updated = await adminAPI.accounts.clearRateLimit(a.id) + patchAccountInList(updated) + appStore.showSuccess(t('common.success')) + } catch (error) { + console.error('Failed to clear rate limit:', error) + } +} const handleDelete = (a: Account) => { deletingAcc.value = a; showDeleteDialog.value = true } const confirmDelete = async () => { if(!deletingAcc.value) return; try { await adminAPI.accounts.delete(deletingAcc.value.id); showDeleteDialog.value = false; deletingAcc.value = null; reload() } catch (error) { console.error('Failed to delete account:', error) } } const handleToggleSchedulable = async (a: Account) => { @@ -762,7 +832,17 @@ const handleToggleSchedulable = async (a: Account) => { } } const handleShowTempUnsched = (a: Account) => { tempUnschedAcc.value = a; showTempUnsched.value = true } -const handleTempUnschedReset = async () => { if(!tempUnschedAcc.value) return; try { await adminAPI.accounts.clearError(tempUnschedAcc.value.id); showTempUnsched.value = false; tempUnschedAcc.value = null; load() } catch (error) { console.error('Failed to reset temp unscheduled:', error) } } +const handleTempUnschedReset = async () => { + if(!tempUnschedAcc.value) return + try { + const updated = await adminAPI.accounts.clearError(tempUnschedAcc.value.id) + showTempUnsched.value = false + tempUnschedAcc.value = null + patchAccountInList(updated) + } catch (error) { + console.error('Failed to reset temp unscheduled:', error) + } +} const formatExpiresAt = (value: number | null) => { if (!value) return '-' return formatDateTime(